﻿using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices;
using System.Windows;
using Pfz.Caching;
using Pfz.DataTypes;
using Pfz.Threading.Contexts;

namespace Pfz.WpfControls
{
	/// <summary>
	/// Base class for PropertyBoundControls that are generic.
	/// </summary>
	public abstract class PropertyBoundControlBase:
		BoundControl
	{
		internal void _Register(object dataContext)
		{
			if (dataContext == null)
				return;

			var hashset = PropertyBoundControl._boundControls.GetOrCreateValue(dataContext);
			hashset.Add(this);
		}
		internal void _Unregister(object dataContext)
		{
			if (dataContext == null)
				return;

			WeakHashSet<PropertyBoundControlBase> hashset;
			if (PropertyBoundControl._boundControls.TryGetValue(dataContext, out hashset))
				hashset.Remove(this);
		}

		/// <summary>
		/// Gets or sets the value of this controls.
		/// </summary>
		[SuppressMessage("Microsoft.Naming", "CA1721:PropertyNamesShouldNotMatchGetMethods")]
		public abstract object Value { get; set; }
		
		/// <summary>
		/// Reads (refreshes) the value of the control from the data-source.
		/// </summary>
		public abstract void ReadFromDataSource();

		/// <summary>
		/// Sets the actual value of the control to the data-source.
		/// </summary>
		public abstract void ApplyToDataSource();
	}

	/// <summary>
	/// A control bound to a property of an object.
	/// </summary>
	/// <typeparam name="ObjectType">The type of the DataSource property.</typeparam>
	public class PropertyBoundControl<ObjectType>:
		PropertyBoundControlBase
	{
		private ValueControl _valueControl = new ValueControl();
		
		/// <summary>
		/// Creates a new PropertyBoundControl instance.
		/// </summary>
		public PropertyBoundControl()
		{
			_valueControl.ValueChanged += _ValueControl_ValueChanged;
			_valueControl.ValueChangeThrownException += _ValueControl_ValueChangeThrownException;
			Content = _valueControl;
            DataContextChanged += _DataContextChanged;
		}

        private void _DataContextChanged(object sender, DependencyPropertyChangedEventArgs e)
        {
			_Unregister(e.OldValue);

            ReadFromDataSource();

			_Register(e.NewValue);
        }

		void _ValueControl_ValueChangeThrownException(object sender, ValueExceptionEventArgs args)
		{
			args.RoutedEvent = ValueControl.ValueChangeThrownExceptionEvent;
			RaiseEvent(args);
		}

		private void _ValueControl_ValueChanged(object sender, ValueChangedEventArgs args)
		{
			OnValueChanged(args);
		
			ApplyToDataSource();
		}

		private PropertyBinding _propertyBinding;
		
		/// <summary>
		/// Gets or sets the PropertyBinding of this control.
		/// </summary>
		public PropertyBinding PropertyBinding
		{
			get
			{
				return _propertyBinding;
			}
			set
			{
				if (value == _propertyBinding)
					return;

				var dataContext = DataContext;
				_Unregister(dataContext);

				if (value != null)
					value.Freeze();

				_propertyBinding = value;
				CreateContentNowIfNeeded();
				_Register(dataContext);
			}
		}

		/// <summary>
		/// Applies the changes done to this control to its data-source.
		/// </summary>
		public override void ApplyToDataSource()
		{
			var binding = _propertyBinding;
			if (binding == null)
				return;
			
			var property = binding.Property;
			if (property == null)
				return;
			
			var dataSource = DataSource;
			if (dataSource == null)
				return;
			
			if (_IsReadOnly())
				return;
			
			using(new ThreadErrors(dataSource))
			{
				try
				{
					object value = Value;
					property.SetValue(dataSource, value, null);
				}
				catch(Exception exception)
				{
					ThreadErrors.AddError(property, exception.GetBaseException().Message);
				}

				if (ThreadErrors.HasErrors)
				{
					_CorrectErrors(ThreadErrors.GetGlobalErrors());

					foreach(var hashset in ThreadErrors.GetErrorsDictionary().Values)
						_CorrectErrors(hashset);
				}
			}
		}

		private void _CorrectErrors(HashSet<object> hashSet)
		{
			List<object> errorsToCorrect = new List<object>();
			foreach(var error in hashSet)
				if (!(error is PropertyError))
					errorsToCorrect.Add(error);

			var propertyInfo = _propertyBinding.Property;
			foreach(var error in errorsToCorrect)
			{
				hashSet.Remove(error);
				hashSet.Add(new PropertyError(propertyInfo, error));
			}
		}
		
		private bool _canShowDisplayName = true;
		
		/// <summary>
		/// Gets or sets a value indicating is the DisplayName can be shown.
		/// </summary>
		[DefaultValue(true)]
		public bool CanShowDisplayName
		{
			get
			{
				return _canShowDisplayName;
			}
			set
			{
				if (value == _canShowDisplayName)
					return;

				_canShowDisplayName = value;
				
				if (value)
				{
					var binding = _propertyBinding;
					if (binding == null)
						_valueControl.DisplayName = null;
					else
						_valueControl.DisplayName = binding.ToString();
				}
				else
					_valueControl.DisplayName = null;
			}
		}
		
		/// <summary>
		/// Gets or sets the object that is read/written as the data-source.
		/// </summary>
		public ObjectType DataSource
		{
			get
			{
				var result = DataContext;
				if (!(result is ObjectType))
					return default(ObjectType);
				
				return (ObjectType)result;
			}
			set
			{
                DataContext = value;
			}
		}

		/// <summary>
		/// Gets or sets the value of this controls.
		/// </summary>
		public override object Value
		{
			get
			{
				return _valueControl.Value;
			}
			set
			{
				_valueControl.Value = value;
			}
		}
		
		/// <summary>
		/// Reads (refreshes) the value of the control from the data-source.
		/// </summary>
		public override void ReadFromDataSource()
		{
			var dataSource = DataSource;
			if (dataSource == null)
			{
				_valueControl.Clear();
				_valueControl.IsReadOnly = true;
				return;
			}
			
			_valueControl.IsReadOnly = _IsReadOnly();
			_valueControl.Value = _GetBoundValue();
		}

		private bool _IsReadOnly()
		{
			var binding = _propertyBinding;
			if (binding == null)
				return true;
			
			var property = binding.Property;
			if (property == null)
				return true;
			
			if (!property.CanWrite)
				return true;
				
			return IsReadOnly || BoundControl.IsDataSourceReadOnly(DataSource);
		}
		private object _GetBoundValue()
		{
			var dataSource = DataSource;
			if (dataSource == null)
				return null;
			
			var binding = _propertyBinding;
			if (binding == null)
				return null;
			
			var property = binding.Property;
			if (property == null)
				return null;
			
			return property.GetValue(dataSource, null);
		}

		/// <summary>
		/// Creates its content.
		/// </summary>
		protected override void OnCreateContent()
		{
			base.OnCreateContent();

			var binding = _propertyBinding;
			if (binding == null)
			{
				_valueControl.DataType = null;
				_valueControl.DisplayName = null;
				return;
			}
			
			if (_canShowDisplayName)
				_valueControl.DisplayName = binding.ToString();
			else
				_valueControl.DisplayName = null;
				
			var property = binding.Property;
			if (property != null)
				_valueControl.DataType = property.PropertyType;
			
			ReadFromDataSource();
		}
		
		/// <summary>
		/// Method invoked when the value of this control changes.
		/// </summary>
		protected virtual void OnValueChanged(ValueChangedEventArgs args)
		{
			args.RoutedEvent = ValueControl.ValueChangedEvent;
			RaiseEvent(args);
		}
		
		/// <summary>
		/// Event invoked when the value of this control changes.
		/// </summary>
		public event EventHandler<ValueChangedEventArgs> ValueChanged
		{
			add
			{
				AddHandler(ValueControl.ValueChangedEvent, value);
			}
			remove
			{
				RemoveHandler(ValueControl.ValueChangedEvent, value);
			}
		}
		
		/// <summary>
		/// Event invoked when an exception is thrown during the process of 
		/// ValueChange.
		/// </summary>
		public event EventHandler<ValueExceptionEventArgs> ValueChangeThrownException
		{
			add
			{
				AddHandler(ValueControl.ValueChangeThrownExceptionEvent, value);
			}
			remove
			{
				RemoveHandler(ValueControl.ValueChangeThrownExceptionEvent, value);
			}
		}

		/// <summary>
		/// Updates the internal Control IsReadOnlyProperty.
		/// </summary>
		protected override void OnIsReadOnlyChanged()
		{
			_valueControl.IsReadOnly = _IsReadOnly();
		}
	}
	
	/// <summary>
	/// A property-bound control that uses the DataSource as a System.Object.
	/// </summary>
	public sealed class PropertyBoundControl:
		PropertyBoundControl<object>
	{
		internal static ConditionalWeakTable<object, WeakHashSet<PropertyBoundControlBase>> _boundControls = new ConditionalWeakTable<object, WeakHashSet<PropertyBoundControlBase>>();
		/// <summary>
		/// Gets all the PropertyBoundControls bound to an specific object.
		/// </summary>
		public static List<PropertyBoundControlBase> GetControlsBoundTo(object dataSource)
		{
			WeakHashSet<PropertyBoundControlBase> hashset;
			if (_boundControls.TryGetValue(dataSource, out hashset))
				return hashset.ToList();

			return null;
		}

		/// <summary>
		/// Applies all changes from PropertyBoundControls bound to the given dataSource.
		/// </summary>
		public static void ApplyToDataSource(object dataSource)
		{
			if (dataSource == null)
				throw new ArgumentNullException("dataSource");

			WeakHashSet<PropertyBoundControlBase> controls;
			if (!_boundControls.TryGetValue(dataSource, out controls))
				return;

			using(new ThreadErrors(dataSource))
				foreach(var control in controls)
					control.ApplyToDataSource();
		}
	}
}
